Published on

재빌드 없이 env 변동사항 적용하기

Authors
  • avatar
    Name
    CDD
    Twitter

서론

분야가 의료쪽이다 보니 되게 독특한 요구사항들이 자주 발생하는 것이 저희 회사입니다. 이번에는 온프레미스 환경을 위해 패키징을 만드는 일을 담당하기로 했었습니다. 물론 패키징을 하는 것도 중요하지만, 첫 번째 걸림돌이 바로 아래와 같은 요구사항이었습니다.

- 서버에서는 절대 빌드하지 말 것
- 원본 소스를 서버에 설치하지 말 것

약간 설치 마법사 같이 설치파일 하나만으로 제품을 설치하는 것을 원하시는 것 같았습니다. 거기다 의료 관련 제품이기 때문에 사용하는 곳도 병원이고, 그렇기 때문에 개발자가 원격으로 제품을 직접 설치할수가 없어서 최대한 간단하게 패키징 해야 했죠.

다시 본론으로 돌아와서 baseURL과 같은 도메인 주소는 .env 같은 곳에 넣어서 관리 중이었는데, 그러면 병원 마다 패키징을 따로 해서 배포를 할 순 없잖아요? 그래서 이를 해결하기 위한 고민을 좀 해봤습니다.

원래는 어떻게 했는데..?

정말 간단하게 설명하자면 외부 설정 파일을 하나 두고, 해당 파일을 참조하는 방식으로 진행했었습니다. 이렇게 되니 설치 할 때마다 설정 파일을 수정해야 한다는 번거로움이 생깁니다. 거기다 request를 던져서 가져오는 방식이다보니 로직도 지저분해지는 단점 또한 존재했죠.

현재의 Next.js 빌드 방식

Next.js를 서버로써 구동시키다 보니 리소스를 많이 잡아먹는 등 여러가지 이슈가 생겨서 저희는 Static Export를 진행하고 있었습니다. 서버리스 기반으로 빌드된 파일만 Nginx에 할당해주는 방식이죠. 장점이라고 한다면 서버가 리소스를 많이 먹거나 하는 걱정이 없다는 것인데, Next의 여러 기능들을 포기해야 하는 것과 같은 단점이 있긴 하죠.

Runtime Env..?

해결책을 고민하면서 서칭하다가 발견한 것이 바로 Runtime Env, 즉 서버가 실행될 때 Env를 설정할 수 있는 방법이었습니다. 정말 다행히도 Next 공식 문서를 보니 이에 대한 내용이 있더라고요.

Component.tsx
// 방법 1
import { unstable_noStore as noStore } from 'next/cache'

export default function Component() {
  noStore()
  const value = process.env.MY_VALUE
}
next.config.js
// 방법 2
module.exports = {
  serverRuntimeConfig: {
    // Will only be available on the server side
    mySecret: 'secret',
    secondSecret: process.env.SECOND_SECRET, // Pass through env variables
  },
  publicRuntimeConfig: {
    // Will be available on both server and client
    staticFolder: '/static',
  },
}

// 사용부
import getConfig from 'next/config'
import Image from 'next/image'

const { serverRuntimeConfig, publicRuntimeConfig } = getConfig()

function MyImage() {
  return (
    <div>
      <Image
        src={`${publicRuntimeConfig.staticFolder}/logo.png`}

뭐, 이런 두 가지 옵션이 있기는 한데, 이 두 방식에서는 치명적인 단점이 하나 있었습니다. 바로 SSR이 아니라면 해당 값을 참조하는 것이 안된다는 것입니다. 사실 저희는 axios를 사용하고 있기에 인스턴스를 생성할 때 아래와 같이 유틸 함수 느낌으로 생성하거든요.

wrapper.ts
import axios, { AxiosResponse, AxiosRequestConfig } from "axios";

const axiosInstance = axios.create({
  headers: {},
});

axiosInstance.interceptors.request.use(
  (config) => {
    config.baseURL = process.env.BACKEND_URL;

위의 두 방식으로 실험해 본 결과 안타깝게도 위의 방식들은 다 undefined를 반환했습니다. 그렇지만 저만 이런 상황에 놓여진 건 아니겠죠, 계속해서 서칭해 본 결과 정말 신박한 해결책을 찾을 수 있었습니다. 아, 참고로 맨 아래에 참고 링크도 붙여놨습니다.

Solution

config.js
const publicEnv = {
  DOMAIN: process.env.DOMAIN,
};

export default function handler(_req, res) {
  res.status(200).send(`window.PUBLIC_CONFIG = ${JSON.stringify(publicEnv)}`);
}

엥..? 이게 무슨 코드야?

맨 아래에 handler 함수를 보면 프론트에서 사용하는 코드와 거리가 멀어보이지 않나요? 네, 맞습니다! 이건 바로 response를 주는 전형적인 백엔드 코드에요. 백엔드는 요청이 들어오면 windowenv를 심어주는 스크립트를 돌려주는 식으로 되는겁니다.

아니, 저건 백엔드가 구현 해줘야 하는 거 아냐..?

아닙니다, 저희는 Next.js를 사용하고 있기 때문이죠. 아래와 같이 디렉토리 구조를 짜서 config.js 파일을 넣어줍니다.

pages/
  ├── api/
  │   ├── config.js

이렇게 하면 localhost/api/config로 접근이 가능해지는 것이죠. 그리고 이를 적용하는 가장 간단한 방법, 그냥 스크립트로 심어줍니다.

_document.tsx
export default function Document() {
  return (
    <Html lang="en">
      <Head>
        <script src={"/api/config"} defer /> // Next 서버로 /api/config를 날리는 부분
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

이렇게 하면 window.PUBLIC_CONFIG는 번들링에서 벗어나게 되고, 서버를 실행시키는 런타임 시점에서 env가 선언되는 것이죠. 물론 단점은 존재합니다, 시크릿 키와 같은 중요한 정보를 담기에는 보안성이 떨어진다 정도?

그래, 이제 서버를 사용하자..

결국에는 다시 정적 배포에서 Next 서버로 돌아왔습니다. 위의 방법을 위해서는 서버가 돌아가야 하거든요. 하지만 이제부터는 .env만 변경해줘도 곧바로 변경사항을 적용할 수 있다는 장점이 생겼습니다. 다시 배포하고, 다시 다운 받을 일이 적어졌다는 말이죠. 모두가 조금 더 편하게 쓸 수 있도록 공통 라이브러리에 아래와 같은 함수를 추가해줬습니다.

util-system
export const getPublicConfig = (envKey) =>
  typeof window === "undefined"
    ? publicEnv[envKey]
    : window.PUBLIC_CONFIG[envKey];

다음에는 본격적인 패키징에 대한 경험을 좀 더 작성해보려 합니다, 감사합니다.

참고